Skip to content

CLDSRV-863: Checksums for PutObject and UploadPart#6112

Merged
bert-e merged 8 commits intodevelopment/9.4from
improvement/CLDSRV-863-checksums-put-object-part
Mar 26, 2026
Merged

CLDSRV-863: Checksums for PutObject and UploadPart#6112
bert-e merged 8 commits intodevelopment/9.4from
improvement/CLDSRV-863-checksums-put-object-part

Conversation

@leif-scality
Copy link
Contributor

No description provided.

@bert-e
Copy link
Contributor

bert-e commented Mar 16, 2026

Hello leif-scality,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Contributor

bert-e commented Mar 16, 2026

Incorrect fix version

The Fix Version/s in issue CLDSRV-863 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 9.4.0

Please check the Fix Version/s of CLDSRV-863, or the target
branch of this pull request.

@codecov
Copy link

codecov bot commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 99.32886% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.69%. Comparing base (9d9cb06) to head (71a178e).
⚠️ Report is 8 commits behind head on development/9.4.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
lib/auth/streamingV4/trailingChecksumTransform.js 98.07% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

Files with missing lines Coverage Δ
lib/api/apiUtils/integrity/validateChecksums.js 98.80% <100.00%> (+0.84%) ⬆️
lib/api/apiUtils/object/prepareStream.js 100.00% <100.00%> (ø)
lib/api/apiUtils/object/storeObject.js 100.00% <100.00%> (+10.34%) ⬆️
lib/auth/streamingV4/ChecksumTransform.js 100.00% <100.00%> (ø)
lib/services.js 86.96% <100.00%> (+0.06%) ⬆️
lib/auth/streamingV4/trailingChecksumTransform.js 98.19% <98.07%> (+5.74%) ⬆️

... and 2 files with indirect coverage changes

@@                 Coverage Diff                 @@
##           development/9.4    #6112      +/-   ##
===================================================
+ Coverage            84.37%   84.69%   +0.31%     
===================================================
  Files                  206      207       +1     
  Lines                13329    13543     +214     
===================================================
+ Hits                 11247    11470     +223     
+ Misses                2082     2073       -9     
Flag Coverage Δ
file-ft-tests 68.57% <85.57%> (+0.30%) ⬆️
kmip-ft-tests 28.10% <30.20%> (+0.15%) ⬆️
mongo-v0-ft-tests 69.84% <85.57%> (+0.36%) ⬆️
mongo-v1-ft-tests 69.74% <85.57%> (+0.26%) ⬆️
multiple-backend 36.33% <32.55%> (+0.02%) ⬆️
sur-tests 36.39% <27.18%> (+0.69%) ⬆️
sur-tests-inflights 37.42% <27.18%> (-0.13%) ⬇️
unit 70.63% <99.32%> (+0.61%) ⬆️
utapi-v2-tests 34.54% <31.87%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude
Copy link

claude bot commented Mar 16, 2026

LGTM

The checksum pipeline (prepareStream → ChecksumTransform → storeObject validation) is well-structured. The mutable errCb + jsutil.once pattern in storeObject correctly prevents double callbacks across the finish/error race. The defensive err.is && guards in services.js fix a real crash path. Arsenal pinned to tag 8.3.6. Tests cover the key edge cases (chunk boundary splits, trailer parsing, double-callback prevention, batchDelete failures).

Review by Claude Code

@claude
Copy link

claude bot commented Mar 19, 2026

LGTM — well-structured checksum pipeline with good error handling and thorough tests.

- trailingChecksumTransform.js:119: Inconsistent error construction — uses errors.IncompleteBody.customizeDescription(...) inline while the identical error is already a module-level constant (incompleteBodyError on line 5, built from errorInstances). Should reuse the constant for consistency.

Review by Claude Code

return null;
}

if (this.trailerChecksumValue) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateChecksum() uses a truthy check to detect unexpected trailers on line 56. If setExpectedChecksum is called with an empty string value (e.g. from a malformed trailer like x-amz-checksum-crc32: with nothing after the colon), the check evaluates to false and the unexpected trailer goes undetected. Consider using this.trailerChecksumValue !== undefined for consistency with line 27. — Claude Code

@claude
Copy link

claude bot commented Mar 19, 2026

  • ChecksumTransform.validateChecksum() line 56: truthy check on this.trailerChecksumValue can miss an unexpected trailer when the value is an empty string. Use !== undefined instead.

    Review by Claude Code

const dataStream = stripTrailingChecksumStream(dataStreamTmp, log, cbOnce);
return data.put(
cipherBundle, dataStream, size, objectContext, backendInfo, log,
cipherBundle, checksumedStream.stream, size, objectContext, backendInfo, log,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storeObject.js passes checksumedStream.stream (a ChecksumTransform) to data.put, but ChecksumTransform never has .headers set on it. In the old code, prepareStream always returned a stream with .headers = stream.headers. If any data backend or middleware reads .headers from the stream passed to data.put, this will be undefined.

I checked lib/data/wrapper.js and it does not reference .headers, but worth verifying against all backend implementations (sproxyd, azure, gcp, etc.) that none rely on it.

— Claude Code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.headers: checked — no backend in lib/data/ or Arsenal reads .headers off the stream passed to data.put.

if (checksumedStream.stream.writableFinished) {
return doValidate();
}
checksumedStream.stream.once('finish', doValidate);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race: if ChecksumTransform errors during _flush (e.g. crc64nvme digest fails), the error goes through errCb which calls cbOnce. But if writableFinished was false, the doValidate listener is also registered on 'finish'. Since _flush called callback(error), the stream will not emit 'finish' so the listener is harmless — but the 'finish' and error paths are independent, and both can potentially reach cbOnce. The jsutil.once(cb) guard prevents double-callback, but the upgraded onStreamError (line 88-96) could trigger batchDelete even though doValidate also triggers batchDelete on checksum failure. Consider whether both paths could fire in an edge case.

— Claude Code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double-delete: not a risk — finish and error are mutually exclusive in the Node.js stream lifecycle, so both handlers can't fire for the same stream.

@claude
Copy link

claude bot commented Mar 19, 2026

Well-structured PR that adds checksum validation for PutObject and UploadPart across all streaming modes. The ChecksumTransform, trailer parsing, and error mapping are clean. Two items to verify:

- ChecksumTransform (returned by prepareStream) does not have .headers set, unlike the old code which always set .headers on the returned stream. Verify no data backend reads .headers from the stream passed to data.put.
- In storeObject.js, the finish-wait path (line 121) and the upgraded onStreamError (line 88) are both registered after data.put succeeds. If both could fire (unlikely but worth reasoning about), batchDelete could be called twice for the same key. The jsutil.once(cb) prevents double-callback but not double-delete.

Review by Claude Code

@claude
Copy link

claude bot commented Mar 19, 2026

LGTM

The stream pipeline design is solid: ChecksumTransform always wraps the data path, the mutable errCb upgrade pattern in storeObject.js correctly handles cleanup after data.put, and jsutil.once guards prevent double callbacks across piped streams. Arsenal is correctly pinned to tag 8.3.6. The defensive err.is check in services.js is a good drive-by fix. Test coverage is comprehensive across all protocol modes and edge cases.

Review by Claude Code

@leif-scality leif-scality force-pushed the improvement/CLDSRV-863-checksums-put-object-part branch from a284c9f to dade07b Compare March 20, 2026 09:54
@claude
Copy link

claude bot commented Mar 20, 2026

LGTM

Well-structured PR. The new ChecksumTransform pipeline, trailer parsing, once-guard error handling, and the mutable-errCb upgrade pattern in storeObject are all cleanly implemented. The race condition fix in services.js (isCompleteMPUInProgress returning true on NoSuchKey, and the defensive err.is guard in metadataValidateMultipart) is correct. Arsenal pinned to tag 8.3.6. No issues found.

Review by Claude Code

@leif-scality leif-scality force-pushed the improvement/CLDSRV-863-checksums-put-object-part branch from a45ea10 to 65aa501 Compare March 23, 2026 10:52

for (const protocol of protocols) {
for (const algo of algos) {
itSkipIfAWS(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test names using itSkipIfAWS() in makeScenarioTests start with should, but the dynamically generated tests in the for loops (e.g. line 472 below) don't. They should follow the same convention.

— Claude Code

@claude
Copy link

claude bot commented Mar 23, 2026

LGTM — solid implementation of streaming checksum validation for PutObject and UploadPart. The stream pipeline design (V4Transform / TrailingChecksumTransform → ChecksumTransform), the once-guarded error callbacks, and the mutable onStreamError upgrade pattern in storeObject are all well done. The race condition fix in services.js (NoSuchKey guard for isCompleteMPUInProgress) is a good defensive addition.

One minor issue flagged inline:
- tests/functional/raw-node/test/checksumPutObjectUploadPart.js: dynamically generated test names in the for-loops don't start with should

Review by Claude Code

for (const protocol of protocols) {
for (const algo of algos) {
itSkipIfAWS(
`${protocol.name} with wrong x-amz-checksum-${algo.name}: returns 400 BadDigest`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test names passed to itSkipIfAWS() (which wraps it()) should start with should per project convention.

— Claude Code

Copy link
Contributor

@jonathan-gramain jonathan-gramain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +41 to +48
if (!this.streamClosed && this.readingTrailer && this.trailerBuffer.length === 0) {
// Nothing came after "0\r\n", don't fail.
// If the x-amz-trailer header was present then the trailer is required and ChecksumTransform will fail.
return callback();
} else if (!this.streamClosed && this.readingTrailer && this.trailerBuffer.length !== 0) {
this.log.error('stream ended without trailer "\r\n"');
return callback(incompleteBodyError);
} else if (!this.streamClosed && !this.readingTrailer) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be even cleaner with an FSM when state management becomes a bit more complex, by maintaining an explicit state variable and acting based on its value.

if (xAmzChecksumCnt > 1) {
return { error: ChecksumError.MultipleChecksumTypes, details: { algorithms: checksumHeaders } };
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick suggestion for clarity:

  • add const checksumHeader = checksumHeaders[0];
  • thereafter, check if a header is present via if (checksumHeader) and use it directly, instead of checking the count and using checksumHeaders[0]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know indexing an empty array does not throw in JS, but I don't feel comfortable doing it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do:

const checksumHeader = xAmzChecksumCnt > 0 ? checksumHeaders[0] : undefined;

My point was more about using a separate variable since we have zero or one checksum, it clarifies the logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

_transform(chunk, encoding, callback) {
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably assume that it's a Buffer, I don't think we would send string contents to this stream in any case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had issues in the past with encoding, so I prefer to be explicit and always check


_transform(chunk, encoding, callback) {
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
this.hash.update(input, encoding);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since input is a buffer, encoding will be ignored, so better leave it out (if you wanted to support strings, then you would have to pass encoding to the Buffer.from call above, but as I said I don't think we would have to deal with strings).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, removed the encoding argument

this.trailerChecksumValue = undefined;
}

setExpectedChecksum(name, value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like whether the checksum comes from the checksum header or a trailer, they should be treated the same way when it comes to comparing with the computed checksum.
I understand that you defer validation of the checksum value itself to validateChecksum, but maybe it would be best to validate it directly here, then update this.expectedChecksum so the following logic of validateChecksum can be agnostic of where the checksum comes from. (If you really need to defer the error reporting to validateChecksum, you could set an error flag this.checksumError or similar and check it at the beginning of validateChecksum, but it may not be needed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is that when we call setExpectedChecksum we don't know if _flush has been called and finished, so this.digest may not be ready. In dataStore we make sure the checksumed stream is finished before calling validateChecksum, so we know this.digest is ready.

Let me know if this responds your concern or if I missed something

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is that every sanity check done in the this.isTrailer case (https://github.com/scality/cloudserver/pull/6112/changes#diff-5ea0da6c64111c8a94b664848cc7c8e9201b530aa77f0a6e962a905af8055f9eR25) except the final digest check could be done within setExpectedChecksum, where the function would:

  • on success, set this.expectedChecksum to be validated by validateChecksum at the end, i.e. using the same code path than for non-trailer checksum
  • on error, either return a validation error immediately or mark the error in an attribute this.checksumError (for example) to be checked and returned first thing by validateChecksum

Copy link
Contributor Author

@leif-scality leif-scality Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, but if we return an error then we need to handle it in the trailer event handler not great, and setting a variable and then checking it in validateChecksum is not a improvement in my opinion

Copy link
Contributor

@jonathan-gramain jonathan-gramain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will review the new FSM tomorrow

stream.pipe(trailingChecksumTransform);
trailingChecksumTransform.headers = stream.headers;
return trailingChecksumTransform;
let stream = request;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you benefit from creating and overwriting this temporary variable, you could just use request or middleware streams like v4Transform directly where stream is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

const doValidate = () => {
const checksumErr = checksumedStream.stream.validateChecksum();
if (checksumErr) {
log.debug('failed checksum validation stream', checksumErr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.debug('failed checksum validation stream', checksumErr);
log.debug('failed checksum validation stream', { error: checksumErr });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

});
});

it('should not delete stored data when checksum validation passes', done => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is marked as "checksum validation passes" but it seems there is no checksum passed at all, is it meant to pass a valid x-amz-checksum-crc32 header matching the hello world payload for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, added checksum

];

// Build the raw chunked body for STREAMING-UNSIGNED-PAYLOAD-TRAILER
function buildTrailerBody(algoName, wrongDigest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick on the variable name wrongDigest: technically this function doesn't care if the digest is correct or not, so it could just be called digest (meaning it could also potentially be used to build correct bodies)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with refactor of trailer body creation

// Constants for protocol scenario tests

const trailerContent = Buffer.from('trailer content'); // 15 bytes, hex 'f'
const trailerContentSha256 = '4x74k2oA6j6knXzpDwogNkS6E3MM49tPpJMjfD+ES68=';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const trailerContentSha256 = '4x74k2oA6j6knXzpDwogNkS6E3MM49tPpJMjfD+ES68=';
const trailerContentSha256 = crypto.createHash('sha256').update(trailerContent).digest('base64');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with refactor of trailer body creation


// Create the 24 common protocol-scenario tests for a given URL factory.
// urlFn() is called lazily at test runtime so that uploadId is available.
function makeScenarioTests(urlFn) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be worth adding some tests with larger synthetic bodies (e.g. 10MB of As), both with correct and wrong checksum, to check that streaming checksumming works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const byte = data[idx++];
if (byte === CR && this.bufferOffset === 0) {
log.error('unexpected CR at start of chunk length');
return errorInstances.InvalidArgument;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't use customizeDescription you can use errors to have a new instance with the current stacktrace, if passing an error with a fresh stacktrace matters.

Otherwise you'd have a pre instanciated error without .stack trace. https://github.com/scality/Arsenal/blob/362f7908b6126709add96ee981d5235d96bab58c/lib/errors/index.ts#L196-L214

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
const chunkLen = parseInt(chunkLenStr, 16);

if (chunkLen > maximumAllowedPartSize) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rather use maximumAllowedUploadSize ? It's for PutObject, even if it's the same value, the maximumAllowedPartSize is for MPU (with lower value for MPU_TESTING)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this transform is used for both cases

});
});

describe('TrailingChecksumTransform trailer parsing and emitting', () => {
Copy link
Contributor

@BourgoisMickael BourgoisMickael Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be inside the above top-level describe, to keep only 1 top-level describe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

assert.deepStrictEqual(err, errors.IncompleteBody);
});

it('should return IncompleteBody when stream ends in READ_TRAILER_CHECKSUM_LF (trailer CR seen, no LF)',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: trailer -> trailing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

assert.deepStrictEqual(err, errors.InvalidArgument);
});

it('should return InvalidArgument when trailer CR is not followed by LF', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailer -> trailing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

READ_CHUNK_DATA: 2,
READ_CHUNK_DATA_CR: 3,
READ_CHUNK_DATA_LF: 4,
COMPLETED: 5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to make it more explicit

Suggested change
COMPLETED: 5,
COMPLETED_NO_TRAILER: 5,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

COMPLETED: 5,
READ_TRAILER_CHECKSUM: 6,
READ_TRAILER_CHECKSUM_LF: 7,
END: 8,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same idea

Suggested change
END: 8,
COMPLETED_WITH_TRAILER: 8,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +53 to +55
if (byte === CR && this.bufferOffset === 0) {
log.error('unexpected CR at start of chunk length');
return errorInstances.InvalidArgument;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor suggestion for simplification: defer the "empty length" special error case to the length parsing stage, which I find a bit cleaner in terms of responsibility:

  1. accumulate until \r\n
  2. parse and validate what was obtained

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// bufferOffset is reset here so the shared buffer is clean for the trailer.
this.state = State.READ_TRAILER_CHECKSUM;
this.bufferOffset = 0;
// falls through
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't advise relying on fall through (a legacy from C language, can cause subtle bugs), here a continue would work fine, letting the FSM switch normally to the next state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@leif-scality leif-scality force-pushed the improvement/CLDSRV-863-checksums-put-object-part branch 3 times, most recently from 1937c20 to 1167cf2 Compare March 26, 2026 13:32
@claude
Copy link

claude bot commented Mar 26, 2026

LGTM

The checksum pipeline is well-structured: ChecksumTransform as the terminal stream, the FSM-based TrailingChecksumTransform, the mutable errCb upgrade pattern in storeObject with jsutil.once guards, and the writableFinished/finish-event handling for async digest computation all look correct. The stream wiring order (trailer event fires before end propagates, so setExpectedChecksum is called before _flush) is sound. Arsenal pinned to tag 8.3.8. Tests are thorough with good edge-case coverage.

Review by Claude Code

@leif-scality leif-scality force-pushed the improvement/CLDSRV-863-checksums-put-object-part branch from 1167cf2 to 71a178e Compare March 26, 2026 14:09
@claude
Copy link

claude bot commented Mar 26, 2026

LGTM

Solid implementation of streaming checksum validation for PutObject and UploadPart. The FSM-based TrailingChecksumTransform rewrite, the ChecksumTransform pipeline, and the mutable error handler pattern in storeObject.js are all well-designed with proper once-guards to prevent double callbacks.

Review by Claude Code

@leif-scality
Copy link
Contributor Author

ping

1 similar comment
@leif-scality
Copy link
Contributor Author

ping

@bert-e
Copy link
Contributor

bert-e commented Mar 26, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

@leif-scality
Copy link
Contributor Author

/approve

@bert-e
Copy link
Contributor

bert-e commented Mar 26, 2026

I have successfully merged the changeset of this pull request
into targetted development branches:

  • ✔️ development/9.4

The following branches have NOT changed:

  • development/7.10
  • development/7.4
  • development/7.70
  • development/8.8
  • development/9.0
  • development/9.1
  • development/9.2
  • development/9.3

Please check the status of the associated issue CLDSRV-863.

Goodbye leif-scality.

The following options are set: approve

@bert-e bert-e merged commit 71a178e into development/9.4 Mar 26, 2026
31 checks passed
@bert-e bert-e deleted the improvement/CLDSRV-863-checksums-put-object-part branch March 26, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants